package cmich.cps.svn.utility;

import java.awt.Desktop;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import javax.swing.SwingWorker;

import org.apache.commons.io.FileUtils;
import cmich.cps.svn.utility.gui.Prompts;

/**
 * The Grader Class.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 */
public class Grader extends SwingWorker<Void, Void> {

	/** The Constant LOGGER. */
	private final static Logger LOGGER = Logger.getLogger(Grader.class.getName());

	/** The system. */
	private UtilitySystem system;

	/** The settings. */
	private Settings settings;

	/** The prompts. */
	private Prompts prompts;

	/** The repository. */
	private Repository repository;

	/** The assignment. */
	private Assignment assignment;

	/** The grading dir. */
	private File gradingDir;

	/** The unit tests. */
	private List<File> unitTests = new ArrayList<File>();

	/** The test names. */
	private List<String> testNames = new ArrayList<String>();

	/** The results. */
	private List<List<String>> results;

	/** The unit test packages. */
	private List<String> unitTestPackages = new ArrayList<String>();

	/** The report file in HTML format. */
	private File reportFile;

	/**
	 * Instantiates a new grader.
	 */
	public Grader() {
		system = UtilitySystem.getInstance();
		settings = Settings.getInstance();
		prompts = Prompts.getInstance();
		repository = system.getActiveRepo();		
		assignment = repository.getActiveAssignment();
		gradingDir = new File(settings.getLocalRepoDirectory() + File.separator + "Grading");
		this.gradingDir.mkdir();
	}

	/**
	 * Begins grading of the {@link #assignment}
	 */
	public void grade() {
		if(assignment.isPastDue() || prompts.confirm("Assignment is not past the due date, are you sure you want to grade it now?")) {
			this.results = new ArrayList<List<String>>();
			if(setUnitTests()) {
				List<String> students = repository.getStudents();
				setHeaders();
				setProgress(1);
				for(int i=0;i<students.size();i++) {
					gradeStudent(students.get(i));
					int progress = (i+1) * (100 / students.size());
					setProgress(progress);
				}
				generateReport();
			}
		}
	}

	/**
	 * Tests the students code with the {@link #unitTests}
	 *
	 * @param student the student's global id
	 */
	private void gradeStudent(String student) {
		File studentDir = new File(assignment.getPath() + File.separator + student);
		File studentSrcDir = new File(studentDir.getPath() + File.separator + "src");
		File studentGradeDir = new File(this.gradingDir + File.separator + student);
		results.get(0).add(student);
		if(!studentDir.exists()) {
			gradingFailure("Student directory does not exist");
		}
		else {
			if(copyUnitTests(student) && copyDirectory(studentSrcDir, student) && compile(studentGradeDir)) {
				setOnTime(system.info(studentDir.getAbsolutePath()));
				String testClasses = "";
				for(int i=0;i<unitTestPackages.size();i++) {
					testClasses = testClasses + " " + this.unitTestPackages.get(i);
				}
				String result = system.execCommand("java -cp " + getJarFile() + File.pathSeparator + settings.enclose(studentGradeDir.getPath()) + " org.junit.runner.JUnitCore" + testClasses);
				if(result.startsWith("Command timed out.")) {
					gradingFailure("Student has an infinite loop in program, add timeout to test for better results. Ex: \"@Test(timeout=1000)\"");
				}
				else if(result.contains("Could not find class")) {
					String msg = "Could not find class: " + testClasses + ". This is likely due to a compiler error, make sure javac is accessible.";
					LOGGER.warning(msg);
					prompts.nonFatalError(msg);
					gradingFailure(msg);
				}
				else {
					processResults(result);
				}
			}
		}
		try {
			FileUtils.deleteDirectory(studentGradeDir);
		} catch (IOException e) {
			LOGGER.warning("Couldn't delete " + studentGradeDir);
			prompts.nonFatalError("Couldn't delete student folder: " + studentGradeDir + ", please delete manually.");
		}
	}

	/**
	 * Sets whether the assignment was submitted on time.
	 *
	 * @param info "Yes" if the assignment is on time, otherwise "No" or errors if there are errors
	 */
	private void setOnTime(String info) {
		String submissionTime = info.split("Last Changed Date: ")[1].split(" -0")[0];
		SimpleDateFormat svnSDF = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
		String result;
		try {
			Date submitTime = svnSDF.parse(submissionTime);
			if(submitTime.before(assignment.getDueDate())) {
				result = "Yes :: Submitted at: " + svnSDF.format(submitTime);
			}
			else {
				result = "No :: Submitted at: " + svnSDF.format(submitTime);
			}
		} catch (ParseException e) {
			String msg = "Incorrect parsing for submission time; " + e.toString();
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			result = "Error";
		}
		LOGGER.fine("Set on time to: " + result);
		results.get(1).add(result);
	}

	/**
	 * Handles grading failures.
	 * @param msg the grading failure message
	 */
	private void gradingFailure(String msg) {
		LOGGER.finest("Grading failure: " + msg);
		results.get(1).add("Error");
		results.get(2).add("Error");
		results.get(3).add("Error");
		results.get(4).add("Error");
		results.get(5).add("Unable to grade: " + msg);
		for(int i=0;i<this.testNames.size();i++) {
			results.get(i+6).add("Error");
		}
	}

	/**
	 * Processes the unit test results.
	 *
	 * @param result the unit test result
	 */
	private void processResults(String result) {
		LOGGER.fine("Processing results.");
		String testsRun = "";
		String failures = "";
		if(result.contains("failure")) {
			testsRun = result.split("Tests run: ")[1].split(",  Failures: ")[0];
			failures = result.split(",  Failures: ")[1].split("\n")[0];
		}
		else if(result.contains("OK (")) {
			testsRun = result.split("OK \\(")[1].split(" tests\\)")[0];
			failures = "0";
		}
		else {
			String msg = "Unparseable JUnit results: " + result;
			LOGGER.warning(msg);
			prompts.nonFatalError(msg);
			gradingFailure("Unparseable JUnite Results");
			return;
		}
		int numTests = Integer.parseInt(testsRun);
		int numFailed = Integer.parseInt(failures);
		int numPassed = numTests - numFailed;
		float percent = ((numPassed * 100.0f) / numTests);
		results.get(2).add("" + numPassed);
		results.get(3).add(testsRun);
		results.get(4).add(String.format("%.2f", percent) + "%");
		results.get(5).add("");
		for(int i=0;i<this.testNames.size();i++) {
			String passFail = "pass";
			String testName = this.testNames.get(i);
			if(result.contains(testName)) {
				int index = result.indexOf(testName);
				String error = result.substring(index);
				error = error.split("\n")[1];
				passFail = "fail :: " + error;
			}
			results.get(i+6).add(passFail);
		}
	}

	/**
	 * Sets the unit tests.
	 *
	 * @return true, if successful
	 */
	private boolean setUnitTests() {
		boolean result = false;
		File test = prompts.getFileToAdd(null);
		result = validateTest(test);
		if(!result) {
			return false;
		}
		while(prompts.confirm("Would you like to add another unit test for grading?")) {
			test = prompts.getFileToAdd(test.getParentFile());
			result = validateTest(test) || result;
		}
		return result;
	}

	/**
	 * Validates a unit test
	 *
	 * @param test the unit test to be validated
	 * @return true, if successful
	 */
	private boolean validateTest(File test) {
		if(test==null) {
			return false;
		}
		if(!test.getName().endsWith(".java") || !assignment.checkUnitTestFile(test)) {
			String msg = test.getName() + " is an invalid unit test file, please check the specifications.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		if(this.unitTests.contains(test)) {
			String msg = "You already added that unit test.";
			LOGGER.fine(msg);
			prompts.message(msg);
			return true;
		}
		this.unitTests.add(test);
		String source = settings.getJavaSource(test);
		String className = test.getName().replace(".java", "");
		String myPackage = settings.getPackage(source)  + "." + className;
		this.testNames.addAll(settings.getTestNames(source, myPackage));
		this.unitTestPackages.add(myPackage);
		return true;
	}

	/**
	 * Copies a directory.
	 *
	 * @param dir the directory to be copied
	 * @param student the student's global id
	 * @return true, if successful
	 */
	private boolean copyDirectory(File dir, String student) {
		File newDir = new File(this.gradingDir + File.separator + student);
		try {
			FileUtils.copyDirectory(dir, newDir);
			LOGGER.fine("Copied " + dir + " to " + newDir);
		} catch (IOException e) {
			String msg = "Failed to copy " + assignment + " " + dir.getName() + " directory to Grading directory: " + e.toString();
			LOGGER.info(msg);
			prompts.nonFatalError(msg);
			gradingFailure(msg);
			return false;
		}
		return true;
	}

	/**
	 * Copy unit tests.
	 *
	 * @param student the student's global id
	 * @return true, if successful
	 */
	private boolean copyUnitTests(String student) {
		for(File test : this.unitTests) {
			File destination = new File(this.gradingDir + File.separator + student + File.separator + settings.getPackagePlusFileName(test));
			LOGGER.fine("Copying unit test " + test + " to " + destination);
			try {
				FileUtils.copyFile(test, destination);
			} catch (IOException e) {
				String msg = "Grading corrupted: " + e.toString();
				gradingFailure("Couldn't copy unit tests to grading directory");
				LOGGER.warning(msg);
				prompts.nonFatalError(msg);
				return false;
			}
		}
		return true;
	}

	/**
	 * Sets the headers for the {@link #reportFile}
	 */
	private void setHeaders() {
		LOGGER.finest("Adding headers to report.");
		ArrayList<String> studentId = new ArrayList<String>();
		studentId.add("Global Id");
		results.add(studentId);
		ArrayList<String> onTime = new ArrayList<String>();
		onTime.add("On time?");
		results.add(onTime);
		ArrayList<String> passed = new ArrayList<String>();
		passed.add("Passed");
		results.add(passed);
		ArrayList<String> total = new ArrayList<String>();
		total.add("Total");
		results.add(total);
		ArrayList<String> percentage = new ArrayList<String>();
		percentage.add("Percent");
		results.add(percentage);
		ArrayList<String> comments = new ArrayList<String>();
		comments.add("Comments");
		results.add(comments);
		for(int i=0;i<this.testNames.size();i++) {
			ArrayList<String> header = new ArrayList<String>();
			header.add(this.testNames.get(i));
			results.add(header);
		}
	}

	/**
	 * Gets the jar file.
	 *
	 * @return the jar file
	 */
	private String getJarFile() {
		String path = Grader.class.getProtectionDomain().getCodeSource().getLocation().getPath();
		try {
			String decodedPath = URLDecoder.decode(path, "UTF-8");
			File myJar = new File(decodedPath);
			LOGGER.finest("This jar file: " + myJar);
			return settings.enclose(myJar.getPath());
		} catch (UnsupportedEncodingException e) {
			LOGGER.warning(e.toString());
			prompts.fatalError(e.toString());
		}
		return null;
	}

	/**
	 * Compiles the {@link #unitTests}.
	 *
	 * @param studentGradeDir the student grade directory
	 * @return true, if successful
	 */
	private boolean compile(File studentGradeDir) {
		if(this.unitTests.size()<1) {
			String msg = "Can't compile, there are no unit tests!";
			LOGGER.warning(msg);
			prompts.nonFatalError(msg);
			return false;
		}
		for(int i=0;i<this.unitTests.size();i++) {
			File test = this.unitTests.get(i);
			LOGGER.fine("Beginning compile of " + test);
			String result = system.execCommand("javac -cp " + settings.enclose(studentGradeDir.getPath()) + File.pathSeparator + getJarFile() + " " + settings.enclose(studentGradeDir + File.separator + settings.getPackagePlusFileName(test)));
			if(result.startsWith("java.io.IOException: Cannot run program")) {
				String msg = "javac not found, try adding it to the path; " + result;
				LOGGER.config(msg);
				prompts.nonFatalError(msg);
				gradingFailure("\"javac\" not found");
				return false;
			}
			if(!result.equals("")) {
				String msg = "Error: " + result;
				LOGGER.info(msg);
				if(result.contains("does not exist")) {
					gradingFailure("Student files do not exist in repository");					
				}
				else if(result.contains("symbol:   class")) {
					String className = result.replace("\n", " ").replaceFirst("(.*symbol:   class )(\\w+)(.*)", "$2");
					gradingFailure("Student missing " + className + " class");
				}
				else {
					gradingFailure("Error compiling");
				}
				return false;
			}
		}
		return true;
	}

	/**
	 * Generates {@link #reportFile}
	 */
	private void generateReport() {
		LOGGER.fine("Generating result file.");
		Date date = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_h-mm_a");
		String timestamp = sdf.format(date);
		String fileName = "Grade_Report_-_" + assignment + "_-_" + timestamp;
		File resultFile = new File(this.gradingDir + File.separator + fileName + ".htm");
		FileWriter writer = null;
		try {
			writer = new FileWriter(resultFile);
			String title = "<title>" + fileName + "</title>\n";
			String css = "<style type=\"text/css\">\n"
					+ "table {border: 1px solid black}\n"
					+ "td {height:100%}\n"
					+ "th.test {font-size:75%}\n"
					+ "td.pass, td.Yes {background-color: #75FF47}\n"
					+ "td.fail, td.No {background-color: #FF3D3D}\n"
					+ "td.Error {background-color: #FFFF7A}\n"
					+ "</style>\n";
			String head = "<head>\n" + title + css + "</head>\n";
			String table = "<body>\n<table border=\"1\">\n";
			String html = "<!DOCTYPE html>\n<html>\n" + head + table;
			writer.write(html);
			try {
				for(int i=0;i<results.get(0).size();i++) {
					String line = "";
					for(int j=0;j<results.size();j++) {
						List<String> student = results.get(j);
						String cell = student.get(i);
						line = line + processCells(i, j, cell);
					}
					writer.write("<tr>" +line + "</tr>\n");
					LOGGER.finest("Next line: " + line);
				}
			} catch (ArrayIndexOutOfBoundsException e) {
				LOGGER.warning(e.toString());
				prompts.nonFatalError(e.toString());
			}
			String footer = "</table>\n</body>\n</html>";
			writer.write(footer);
			writer.close();
		} catch (IOException e) {
			String msg = "Cannot write to file " + resultFile + ": " + e.toString();
			LOGGER.warning(msg);
			prompts.fatalError(msg);
		}
		this.reportFile = resultFile;
	}

	/**
	 * Displays {@link #reportFile}
	 */
	public void displayReport() {
		if(this.reportFile!=null && prompts.confirm("Would you like to open the grade report in your browser now?")) {
			try {
				Desktop.getDesktop().browse(reportFile.toURI());
			} catch (IOException ex) {
				String msg = "Unable to open grade repot: " + ex.toString();
				LOGGER.warning(msg);
				prompts.nonFatalError(msg);
			}
		}
	}

	/**
	 * Process cell contents.
	 *
	 * @param i the x index of the cell
	 * @param j the y index of the cell
	 * @param cell the cell contents
	 * @return the string
	 */
	private String processCells(int i, int j, String cell) {
		String result = "";
		if(i==0) {
			if(j<6) {
				result = "<th>" + cell.replace("(", " (") + "</th>";
			}
			else {
				String testName = cell.split("\\(")[0];
				String testPackage = cell.split("\\(")[1].split("\\)")[0];
				result = "<th title=\"" + testPackage + "\" class=\"test\">" + testName + "</th>";
			}
		}
		else {
			String cellTitle = "";
			if(cell.contains(" :: ")) {
				cellTitle = cell.split(" :: ")[1];
				cell = cell.split(" :: ")[0];
			}
			if(j==1) {
				result = "<td title=\"" + cellTitle + "\" class=\"" + cell + "\">" + cell + "</td>";
			}
			else if(j<6) {
				result = "<td>" + cell + "</td>";
			}
			else {
				result = "<td title=\"" + cellTitle + "\" class=\"" + cell + "\">" + cell + "</td>";
			}
		}
		return result;
	}

	/**
	 * Gets the number of students.
	 *
	 * @return the number of students
	 */
	public int getNumStudents() {
		return repository.getStudents().size();
	}

	/* (non-Javadoc)
	 * @see javax.swing.SwingWorker#doInBackground()
	 */
	@Override
	protected Void doInBackground() throws Exception {
		grade();
		return null;
	}

	/* (non-Javadoc)
	 * @see javax.swing.SwingWorker#done()
	 */
	@Override
	protected void done() {
		displayReport();
	}
}